﻿using System;
using System.Diagnostics;

namespace SfsTools.Core
{
    /// <summary>
    /// Encapsulates the file system.
    /// </summary>
    public class FileSystem : Object, IDisposable
    {
        #region Properties

        /// <summary>
        /// Contains the underlying volume.
        /// </summary>
        private IVolume FVolume;
        /// <summary>
        /// Contains the boot block.
        /// </summary>
        private readonly BootBlock FBootBlock;
        /// <summary>
        /// Gets the boot block.
        /// </summary>
        internal BootBlock BootBlock
        {
            [DebuggerStepThrough]
            get
            {
                return FBootBlock;
            }
        }
        /// <summary>
        /// Contains the index area.
        /// </summary>
        private readonly IndexArea FIndexArea;
        /// <summary>
        /// Gets the index area.
        /// </summary>
        internal IndexArea IndexArea
        {
            [DebuggerStepThrough]
            get
            {
                return FIndexArea;
            }
        }
        /// <summary>
        /// Contains the data area.
        /// </summary>
        private readonly DataArea FDataArea;
        /// <summary>
        /// Gets the data area.
        /// </summary>
        internal DataArea DataArea
        {
            [DebuggerStepThrough]
            get
            {
                return FDataArea;
            }
        }

        /// <summary>
        /// Contains the root directory.
        /// </summary>
        private readonly RootDirectory FRootDirectory;
        /// <summary>
        /// Gets the root directory.
        /// </summary>
        public IDirectory RootDirectory
        {
            [DebuggerStepThrough]
            get
            {
                return FRootDirectory;
            }
        }

        #endregion

        #region Methods

        #region Create new FS

        #region Misc
        /// <summary>
        /// Linear interpolation.
        /// </summary>
        /// <param name="xA">The X coordinate of the first point.</param>
        /// <param name="yA">The Y coordinate of the first point.</param>
        /// <param name="xB">The X coordinate of the second point.</param>
        /// <param name="yB">The Y coordinate of the second point.</param>
        /// <param name="x">The X coordinate of the result.</param>
        /// <returns>The Y coordinate of the result.</returns>
        /// <remarks>Assumes xB &gt; xA and yB &gt; yA.</remarks>
        [DebuggerStepThrough]
        private static long Interpolate(long xA, long yA, long xB, long yB, long x)
        {
            return (yB - yA) * (x - xA) / (xB - xA) + yA;
        }
        #endregion

        /// <summary>
        /// Creates a new <see cref="FileSystem"/> on the specified <paramref name="volume"/>, overwriting any existing filesystem or data.
        /// </summary>
        /// <param name="volume">The volume.</param>
        /// <param name="parameters">Creation parameters.</param>
        /// <returns>A <see cref="FileSystem"/> instance.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="volume"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public static FileSystem Create(IVolume volume, FileSystemCreationParameters parameters)
        {
            if (volume == null)
                throw new ArgumentNullException("volume");
            // Create boot block.
            TimeStamp now = TimeStamp.Now;
            BootBlock bb = new BootBlock(volume);
            bb.BootSignature = 0xAA55;
            bb.MagicNumberWithVersionNumber = 0x10534653;
            bb.TimeStamp = now;
            // Start validating creation parameters.
            if (parameters == null)
            {
                parameters = new FileSystemCreationParameters();
                parameters.VolumeSize = volume.Length;
                parameters.BlockSize = 512;
            }
            else if (parameters.VolumeSize < 0)
            {
                parameters.VolumeSize = volume.Length;
            }
            else if (parameters.VolumeSize < 1024)
            {
                // Too small a volume.
                parameters.VolumeSize = 1024;
                parameters.ReservedAreaSize = 512;
                parameters.IndexAreaSize = 512;
                parameters.DataAreaSize = 0;
                parameters.BlockSize = 512;
            }
            else if (parameters.VolumeSize > volume.Length)
            {
                // Volume is not this large.
                parameters.VolumeSize = volume.Length;
            }
            bb.BlockSize = parameters.BlockSize;
            // Truncate volume size, not round it.
            long volumeSize = (parameters.VolumeSize) & (~(bb.BlockSize - 1));
            bb.VolumeSize = volumeSize;
            // Fill in reserved area size.
            if (parameters.ReservedAreaSize < 0)
                parameters.ReservedAreaSize = 0;
            parameters.ReservedAreaSize = Math.Max(bb.BlockSize, (parameters.ReservedAreaSize + bb.BlockSize - 1) & (~(bb.BlockSize - 1)));
            bb.ReservedAreaSize = parameters.ReservedAreaSize;
            // Calculate 'usable' volume size.
            long usableSize = volumeSize - parameters.ReservedAreaSize;
            // Correct erroneous IA sizes.
            if (parameters.IndexAreaSize >= 0 && parameters.IndexAreaSize < bb.BlockSize)
            {
                parameters.IndexAreaSize = bb.BlockSize;
            }
            else if (parameters.IndexAreaSize >= 0)
            {
                parameters.IndexAreaSize = (parameters.IndexAreaSize + bb.BlockSize - 1) & (~(bb.BlockSize - 1));
            }
            // Correct erroneous DA sizes.
            if (parameters.DataAreaSize > 0)
            {
                parameters.DataAreaSize = (parameters.DataAreaSize + bb.BlockSize - 1) & (~(bb.BlockSize - 1));
                if (parameters.DataAreaSize + parameters.IndexAreaSize > usableSize)
                    parameters.DataAreaSize = usableSize - parameters.IndexAreaSize;
            }
            // If both DA and IA present, ensure that they are less than the usable space.
            if (parameters.IndexAreaSize > 0 && parameters.DataAreaSize > 0)
            {
                if (parameters.DataAreaSize + parameters.IndexAreaSize > usableSize)
                {
                    // Scale DA and IA.
                    long desired = parameters.IndexAreaSize + parameters.DataAreaSize;
                    parameters.IndexAreaSize = parameters.IndexAreaSize * usableSize / desired;
                    parameters.DataAreaSize = parameters.IndexAreaSize * usableSize / desired;
                    // Align to block length.
                    parameters.DataAreaSize = (parameters.DataAreaSize + bb.BlockSize - 1) & (~(bb.BlockSize - 1));
                    parameters.IndexAreaSize = (parameters.IndexAreaSize + bb.BlockSize - 1) & (~(bb.BlockSize - 1));
                    // Correct +1..+2 block overflows.
                    if (parameters.DataAreaSize + parameters.IndexAreaSize > usableSize)
                        parameters.DataAreaSize = usableSize - parameters.IndexAreaSize;
                }
            }
            // If IA present, DA absent, fill in DA.
            if (parameters.DataAreaSize < 0 && parameters.IndexAreaSize > 0)
                parameters.DataAreaSize = usableSize - parameters.IndexAreaSize;
            // If IA present, DA absent, fill in DA.
            if (parameters.IndexAreaSize < 0 && parameters.DataAreaSize > 0)
                parameters.IndexAreaSize = usableSize - parameters.DataAreaSize;
            // Auto-guess IA and DA if are absent.
            if (parameters.IndexAreaSize < 0 && parameters.DataAreaSize < 0)
            {
                // Auto-guess index area size.
                // Never allocate more than 5% to index area, and never allocate less than 0.5% to index area.
                // An 1.44M floppy gets 5% while a 512M pendrive gets 0.5%. Inbetween in is approximated linearly.
                // Should be logarithmical, but I dont care that much.
                const long lower = 1440 * 1024;
                const long upper = 512L * 1024 * 1024;
                // measured:
                //10183 index blocks on 128M  3.88%
                // 5830 index blocks on 64M   4.45%
                // 3100 index blocks on 32M   4.73%
                // 1596 index blocks on 16M   4.87%
                //  810 index blocks on 8M    4.94%
                //  408 index blocks on 4M    4.98%
                //  204 index blocks on 2M    4.98%
                //  143 index blocks on 1.44M 4.97%
                //  102 index blocks on 1M    4.98%
                if (volumeSize <= lower)
                {
                    long reserved = usableSize * 5 / 100;
                    reserved = reserved / bb.BlockSize;
                    reserved = Math.Max(1, reserved); // at least one index area block is required.
                    parameters.IndexAreaSize = reserved * bb.BlockSize;
                }
                else if (volume.Length >= upper)
                {
                    long reserved = usableSize * 5 / 1000;
                    reserved = reserved / bb.BlockSize;
                    // Limit the count of index area entries to 1G.
                    if ((reserved / 64) > upper / 2)
                        reserved = (upper / 2) * 64L;
                    parameters.IndexAreaSize = reserved * bb.BlockSize;
                }
                else
                {
                    long reservedA = usableSize * 5 / 100;
                    reservedA = reservedA / bb.BlockSize;
                    reservedA = Math.Max(1, reservedA); // at least one index area block is required.
                    long reservedB = usableSize * 5 / 1000;
                    reservedB = reservedB / bb.BlockSize;
                    // Interpolate.
                    parameters.IndexAreaSize = Interpolate(lower, reservedA, upper, reservedB, volume.Length) * bb.BlockSize;
                }
                parameters.DataAreaSize = usableSize - parameters.IndexAreaSize;
            }
            // Everything ready.
            Debug.Assert(parameters.IndexAreaSize + parameters.DataAreaSize <= usableSize);
            Debug.Assert(parameters.IndexAreaSize % bb.BlockSize == 0);
            Debug.Assert(parameters.DataAreaSize % bb.BlockSize == 0);
            bb.IndexAreaSize = parameters.IndexAreaSize;
            bb.DataAreaSize = parameters.DataAreaSize;
            // Create index entries.
            IndexEntry[] indexAe = new IndexEntry[bb.IndexAreaSize / 64];
            Debug.Assert(indexAe.Length >= 1);
            byte[] smeBuffer = new byte[64]; smeBuffer[0] = 2;
            indexAe[0] = new StartingMarkerEntry(bb.VolumeSize - bb.IndexAreaSize, smeBuffer);
            VolumeIdentifierEntry volID;
            byte[] volIdBuffer = new byte[64]; volIdBuffer[0] = 1;
            indexAe[indexAe.Length - 1] = volID = new VolumeIdentifierEntry(bb.VolumeSize - 64, volIdBuffer);
            volID.VolumeName = new System.Text.StringBuilder().
                Append('V').Append('o').Append('l').Append('u').Append('m').Append('e').Append('_').
                Append(Guid.NewGuid().ToString().Replace('-', '_')).ToString();
            volID.TimeStamp = now;
            DataArea da = new DataArea(volume, bb.DataAreaSize / bb.BlockSize, bb.VolumeSize / bb.BlockSize);
            IndexArea ia = new IndexArea(volume, da, bb.VolumeSize - bb.IndexAreaSize, indexAe);
            // Write back boot block and index area to volume.
            ((IFsComponent)bb).WriteTo(volume);
            ((IFsComponent)ia).WriteTo(volume);
            // And re-parse the newly created file system.
            return new FileSystem(volume);
        }

        /// <summary>
        /// Creates a new <see cref="FileSystem"/> on the specified <paramref name="volume"/>, overwriting any existing filesystem or data.
        /// </summary>
        /// <param name="volume">The volume.</param>
        /// <returns>A <see cref="FileSystem"/> instance.</returns>
        /// <exception cref="ArgumentNullException">The argument <paramref name="volume"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public static FileSystem Create(IVolume volume)
        {
            return Create(volume, null);
        }

        #endregion

        #region Misc

        /// <summary>
        /// Checks that the object is still alive.
        /// </summary>
        [DebuggerStepThrough]
        internal void CheckAlive()
        {
            if (FVolume == null)
                throw new ObjectDisposedException(null);
        }

        #endregion

        /// <summary>
        /// Compacts the index area on the <see cref="FileSystem"/>.
        /// </summary>
        [DebuggerStepThrough]
        public void CompactIndexArea()
        {
            IEntryMoveCallback callback = FRootDirectory.GetRelocationCallback();
            FIndexArea.Compact(callback);
        }

        #region Validation

        /// <summary>
        /// Validates the volume on open.
        /// </summary>
        /// <exception cref="FileSystemValidationException">The volume contains an invalid or damaged <see cref="FileSystem"/>.</exception>
        //[DebuggerStepThrough]
        private void ValidateVolume()
        {
            FIndexArea.SetBlockSize(FBootBlock.BlockSize);
            FDataArea.SetBlockSize(FBootBlock.BlockSize);
            // Ensure block size is within legal and reasonable ranges.
            if (FBootBlock.BlockSize < 256)
                throw new InvalidBlockSizeException(FBootBlock.BlockSize);
            if (FBootBlock.BlockSize > 65536)
                throw new InvalidBlockSizeException(FBootBlock.BlockSize);
            // Ensure magic number matches.
            if (FBootBlock.MagicNumber != 0x534653)
                throw new InvalidMagicNumberException();
            // Note: temporarily disabled.
            //if (FBootBlock.VersionNumber < 0x10)
            //    throw new InvalidVersionNumberException();
            if (FBootBlock.VersionNumber > 0x10)
                Debug.WriteLine("SFS version newer than 1.0");
            if (!FBootBlock.ChecksumValid)
                throw new BootBlockChecksumInvalidException();
            // Ensure that reserved + data + index area size is never larger than the volume size.
            // It can be less, though.
            if (FBootBlock.IndexAreaSize + FBootBlock.DataAreaSize + FBootBlock.ReservedAreaSize > FBootBlock.VolumeSize)
                throw new VolumeSizeMismatchException();
            // Ensure that index area is not too large. This is a limitation of SFStools only.
            if (FBootBlock.IndexAreaSize >= Int32.MaxValue / 64)
                throw new IndexAreaTooLargeException();
            // Read and validate index area.
            FIndexArea.ReadEntries(FVolume, new IndexEntryFactory());
            IndexAreaValidatorVisitor visitor = new IndexAreaValidatorVisitor(FBootBlock, FIndexArea, FDataArea);
            visitor.Validate();
        }

        /// <summary>
        /// Reads the contents of the index area.
        /// </summary>
        [DebuggerStepThrough]
        private void ReadContents()
        {
            IndexAreaListBuilderVisitor visitor = new IndexAreaListBuilderVisitor(this);
            visitor.BuildContents();
        }

        #endregion

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="FileSystem"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="FileSystem"/>.
        /// </summary>
        /// <param name="volume">The <see cref="IVolume"/> to read the file system from.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="volume"/> is <see langword="null"/>.</exception>
        /// <exception cref="FileSystemValidationException">The volume contains an invalid or damaged <see cref="FileSystem"/>.</exception>
        [DebuggerStepThrough]
        public FileSystem(IVolume volume)
            : base()
        {
            if (volume == null)
                throw new ArgumentNullException("volume");
            FVolume = volume;
            FBootBlock = new BootBlock(volume);
            FDataArea = new DataArea(volume, FBootBlock.DataAreaSize / FBootBlock.BlockSize, FBootBlock.VolumeSize / FBootBlock.BlockSize);
            FIndexArea = new IndexArea(volume, FDataArea, FBootBlock.VolumeSize - FBootBlock.IndexAreaSize, (int)(FBootBlock.IndexAreaSize / 64));
            ValidateVolume();
            FRootDirectory = new RootDirectory(this);
            ReadContents();
        }
        #endregion
        
        #region IDisposable Members

        /// <summary>
        /// Releases all resources used by this <see cref="FileSystem"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        public void Dispose()
        {
            if (FVolume != null)
            {
                FVolume.Dispose();
                FVolume = null;
            }
        }

        /// <summary>
        /// Releases all resources used by this <see cref="FileSystem"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        void IDisposable.Dispose()
        {
            if (FVolume != null)
            {
                FVolume.Dispose();
                FVolume = null;
            }
        }

        #endregion
    }
}
